﻿using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration;
using System.Data.Entity.ModelConfiguration.Configuration;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace DomainDriver.DomainModeling.StorageManagers.EntityFrameworkStorage
{
    public static class ForcedMappingUtilities
    {
        public static void CreateForcedMappings(DbContext dbContext, DbModelBuilder modelBuilder)
        {
            Type contextType = dbContext.GetType();
            Type genericDbSetType = typeof(DbSet<>);

            foreach (PropertyInfo propInfo in contextType.GetProperties())
            {
                MethodInfo methodInfo = propInfo.GetGetMethod();
                Type returnType = methodInfo.ReturnType;

                if (!returnType.IsGenericType)
                { continue; }

                Type genericTypeDef = returnType.GetGenericTypeDefinition();

                if (!genericTypeDef.Equals(genericDbSetType))
                { continue; }

                Type entityType = methodInfo.ReturnType.GetGenericArguments().First();

                Type builderType = typeof(DbModelBuilder);
                MethodInfo genericEntityMethod = builderType.GetMethods().Where((MethodInfo mi) => (mi.Name == "Entity")).First();
                MethodInfo actualEntityMethod = genericEntityMethod.MakeGenericMethod(new[] { entityType });
                object mapper = actualEntityMethod.Invoke(modelBuilder, null);

                Type utilitiesType = typeof(ForcedMappingUtilities);
                MethodInfo genericMapMethod = utilitiesType.GetMethods().Where((MethodInfo mi) => (mi.Name == "CreateForcedMappingsForEntityType")).First();
                MethodInfo actualMapMethod = genericMapMethod.MakeGenericMethod(new[] { entityType });
                object result = actualMapMethod.Invoke(utilitiesType, new object[] { mapper });
            }
        }

        public static void CreateForcedMappingsForEntityType<ET>(EntityTypeConfiguration<ET> mapper)
            where ET : class
        {
            Type entityType = typeof(ET);

            foreach (PropertyInfo propInfo in entityType.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance))
            {
                Type propertyType = propInfo.PropertyType;
                bool implementsForceMappedAttribute = propInfo.IsDefined(typeof(ForceMappedAttribute), true);

                if (!implementsForceMappedAttribute)
                { continue; }

                if (propertyType == typeof(string))
                {
                    mapper.MapNonPublicStringProperty<ET>(propInfo.Name);
                }
                else if (propertyType == typeof(byte[]))
                {
                    mapper.MapNonPublicByteArrayProperty<ET>(propInfo.Name);
                }
                else if (propertyType.IsValueType && IsNullableType(propertyType))
                {
                    Type utilitiesType = typeof(ForcedMappingUtilities);
                    MethodInfo genericMapMethod = utilitiesType.GetMethods().Where((MethodInfo mi) => (mi.Name == "MapNonPublicNullableStructProperty")).First();

                    Type actualPropertyType = propertyType.GetGenericArguments()[0];
                    MethodInfo actualMapMethod = genericMapMethod.MakeGenericMethod(new[] { typeof(ET), actualPropertyType });
                    object result = actualMapMethod.Invoke(utilitiesType, new object[] { mapper, propInfo.Name });
                }
                else if (propertyType.IsValueType)
                {
                    Type utilitiesType = typeof(ForcedMappingUtilities);
                    MethodInfo genericMapMethod = utilitiesType.GetMethods().Where((MethodInfo mi) => (mi.Name == "MapNonPublicStructProperty")).First();
                    MethodInfo actualMapMethod = genericMapMethod.MakeGenericMethod(new[] { typeof(ET), propertyType });
                    object result = actualMapMethod.Invoke(utilitiesType, new object[] { mapper, propInfo.Name });
                }
                else
                { throw new InvalidOperationException("Property type is not supported."); }
            }
        }

        public static bool IsNullableType(Type valueType)
        {
            Type nullableType = typeof(Nullable);
            return ((valueType.IsGenericType) && (valueType.FullName.Contains("Nullable")));
        }

        public static void MapNonPublicStructProperty<ENTITY_TYPE, PROP_TYPE>(this EntityTypeConfiguration<ENTITY_TYPE> mapper, String propertyName)
            where ENTITY_TYPE : class
            where PROP_TYPE : struct
        {
            Type entityType = typeof(ENTITY_TYPE);
            ParameterExpression arg = Expression.Parameter(entityType, "x");
            Expression expr = arg;

            PropertyInfo pi = entityType.GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            expr = Expression.Property(expr, pi);

            LambdaExpression lambda = Expression.Lambda(expr, arg);
            Expression<Func<ENTITY_TYPE, PROP_TYPE>> expression = (Expression<Func<ENTITY_TYPE, PROP_TYPE>>)lambda;

            PrimitivePropertyConfiguration propertyConfig = mapper.Property(expression);
            propertyConfig.HasColumnName(propertyName);
        }

        public static void MapNonPublicNullableStructProperty<ENTITY_TYPE, PROP_TYPE>(this EntityTypeConfiguration<ENTITY_TYPE> mapper, String propertyName)
            where ENTITY_TYPE : class
            where PROP_TYPE : struct
        {
            Type entityType = typeof(ENTITY_TYPE);
            ParameterExpression arg = Expression.Parameter(entityType, "x");
            Expression expr = arg;

            PropertyInfo pi = entityType.GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            expr = Expression.Property(expr, pi);

            LambdaExpression lambda = Expression.Lambda(expr, arg);
            Expression<Func<ENTITY_TYPE, Nullable<PROP_TYPE>>> expression = (Expression<Func<ENTITY_TYPE, Nullable<PROP_TYPE>>>)lambda;

            PrimitivePropertyConfiguration propertyConfig = mapper.Property(expression);
            propertyConfig.HasColumnName(propertyName);
        }

        public static void MapNonPublicStringProperty<ENTITY_TYPE>(this EntityTypeConfiguration<ENTITY_TYPE> mapper, String propertyName)
            where ENTITY_TYPE : class
        {
            Type entityType = typeof(ENTITY_TYPE);
            ParameterExpression arg = Expression.Parameter(entityType, "x");
            Expression expr = arg;

            PropertyInfo pi = entityType.GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            expr = Expression.Property(expr, pi);

            LambdaExpression lambda = Expression.Lambda(expr, arg);
            Expression<Func<ENTITY_TYPE, string>> expression = (Expression<Func<ENTITY_TYPE, string>>)lambda;

            PrimitivePropertyConfiguration propertyConfig = mapper.Property(expression);
            propertyConfig.HasColumnName(propertyName);
        }

        public static void MapNonPublicByteArrayProperty<ENTITY_TYPE>(this EntityTypeConfiguration<ENTITY_TYPE> mapper, String propertyName)
            where ENTITY_TYPE : class
        {
            Type entityType = typeof(ENTITY_TYPE);
            ParameterExpression arg = Expression.Parameter(entityType, "x");
            Expression expr = arg;

            PropertyInfo pi = entityType.GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            expr = Expression.Property(expr, pi);

            LambdaExpression lambda = Expression.Lambda(expr, arg);
            Expression<Func<ENTITY_TYPE, byte[]>> expression = (Expression<Func<ENTITY_TYPE, byte[]>>)lambda;

            PrimitivePropertyConfiguration propertyConfig = mapper.Property(expression);
            propertyConfig.HasColumnName(propertyName);
        }
    }
}